<?
/**
 * Copyright 2007 Melange.
 *
 * This file is part of PHP-MVC.
 *
 * PHP-MVC is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * PHP-MVC is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PHP-MVC; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @category    Melange
 * @package     php-mvc
 * @subpackage  util
 * @copyright   Copyright (c) 2007 Jeroen Simons. All rights reserved
 * @author      Jeroen Simons <jeroen@melange.nl>
 * @link        http://www.melange.nl/
 *
 */


/**
 * General purpose abstract class that describes an API for retrieving
 * Locale-sensitive messages from underlying resource locations of an
 * unspecified design, and optionally utilizing the <code>MessageFormat</code>
 * class to produce internationalized messages with parametric replacement.
 * <p> Calls to <code>getMessage()</code> variants without a
 * <code>Locale</code> argument are presumed to be requesting a message string
 * in the default <code>Locale</code> for this JVM. <p> Calls to
 * <code>getMessage()</code> with an unknown key, or an unknown
 * <code>Locale</code> will return <code>null</code> if the
 * <code>returnNull</code> property is set to <code>true</code>.  Otherwise, a
 * suitable error message will be returned instead. <p> <strong>IMPLEMENTATION
 * NOTE</strong> - Classes that extend this class must be Serializable so that
 * instances may be used in distributable application server environments.
  */
class MessageResources {


    // ----------------------------------------------------- Instance Variables


    /**
     * The set of locale keys for which we have already loaded messages, keyed
     * by the value calculated in <code>localeKey()</code>.
     */
    protected $locales = array();


    /**
     * The cache of messages we have accumulated over time, keyed by the value
     * calculated in <code>messageKey()</code>.
     */
    protected $messages = array();


    /**
     * The configuration parameter used to initialize this MessageResources.
     */
    protected $config = null;


    /**
     * Indicate is a <code>null</code> is returned instead of an error message
     * string when an unknown Locale or key is requested.
     */
    protected $returnNull = false;


    /**
     * The default Locale for our environment.
     */
    protected $defaultLocale = null;


    // ----------------------------------------------------------- Constructors


    /**
     * Construct a new PropertyMessageResources according to the specified
     * parameters.
     *
     * @param factory    The MessageResourcesFactory that created us
     * @param config     The configuration parameter for this
     *                   MessageResources
     * @param returnNull The returnNull property we should initialize with
     */
    public function __construct($config, $returnNull) {
        $this->log =& LoggerManager::getLogger("MessageResources");
        if($this->log->isDebugEnabled())
            $this->log->debug("Initializing, config='" . $config . "', returnNull=" . ($returnNull ? "true" : "false"));
        $this->config = $config;
        $this->returnNull = $returnNull;

        require_once("http/Locale.php");
        $this->defaultLocale = Locale::getDefault();
    }


    // --------------------------------------------------------- Public Methods


    /**
     * Returns a text message for the specified key, for the default Locale. A
     * null string result will be returned by this method if no relevant
     * message resource is found for this key or Locale, if the
     * <code>returnNull</code> property is set.  Otherwise, an appropriate
     * error message will be returned. <p> This method must be implemented by
     * a concrete subclass.
     *
     * @param locale The requested message Locale, or <code>null</code> for
     *               the system default Locale
     * @param key    The message key to look up
     * @return text message for the specified key and locale
     */
    public function getMessage(Locale $locale, $key, $placeHolders=null) {

        // Initialize variables we will require
        $localeKey = $this->localeKey($locale);
        $originalKey = $this->messageKey($localeKey, $key);
        $messageKey = null;
        $message = null;
        $underscore = 0;
        $addIt = false; // Add if not found under the original key

        // Loop from specific to general Locales looking for this message
        while (true) {

            // Load this Locale's messages if we have not done so yet
            $this->loadLocale($localeKey);

            // Check if we have this key for the current locale key
            $messageKey = $this->messageKey($localeKey, $key);

            $message = array_key_exists($messageKey, $this->messages) ?
                $this->messages[$messageKey] : null;

            if (!is_null($message)) {
                if ($addIt) {
                    $messages[$originalKey] = $message;
                }
                return $this->format($message, $locale, $placeHolders);
            }

            // Strip trailing modifiers to try a more general locale key
            $addIt = true;
            $underscore = strrpos($localeKey, "_");

            if ($underscore === false) {
                break;
            }

            $localeKey = substr($localeKey, 0, $underscore);

        }

        // Try the default locale if the current locale is different
        if (!$this->defaultLocale == $locale) {
            $localeKey  = $this->localeKey($defaultLocale);
            $messageKey = $this->messageKey($localeKey, $key);
            $this->loadLocale($localeKey);

            $message = array_key_exists($messageKey, $this->messages) ?
                $this->messages[$messageKey] : null;;

            if (!is_null($message)) {
                $messages[$originalKey] = $message;
                return $this->format($message, $locale, $placeHolders);
            }
        }

        // As a last resort, try the default Locale
        $localeKey = "";
        $messageKey = $this->messageKey($localeKey, $key);
        $this->loadLocale($localeKey);

        $message = array_key_exists($messageKey, $this->messages) ?
                $this->messages[$messageKey] : null;

        if (!is_null($message)) {
            $this->messages[$originalKey] = $message;
            return $this->format($message, $locale, $placeHolders);
        }

        // Return an appropriate error indication
        if ($this->returnNull) {
            return null;
        } else {
            return ("???" . $this->messageKey($locale, $key) . "???");
        }


    }


    // ------------------------------------------------------ Protected Methods


    protected function format($message, $locale, $placeHolders) {
        if(!is_null($placeHolders)) {

            if($placeHolders instanceof ActionMessage)
                $placeHolders = array($placeHolders);

            $placeHolderValues = array();
            $replacements = array();
            $i = 0;
            foreach($placeHolders as $placeHolder){
                $placeHolderValues[] = $placeHolder->isResource() ?
                    $this->getMessage($locale, $placeHolder->getKey()) : $placeHolder->getKey();
                $replacements[] = "{".$i++."}";
            }
            return str_replace($replacements, $placeHolderValues, $message);
        }
        return $message;

    }


    /**
     * Compute and return a key to be used in caching information by locale
     * key and message key.
     *
     * @param localeKey The locale key for which this cache key is calculated
     * @param key       The message key for which this cache key is
     *                  calculated
     */
    protected function messageKey($localeKey, $key) {
        return ($localeKey . "." . $key);
    }

    /**
     * Compute and return a key to be used in caching information by a Locale.
     * <strong>NOTE</strong> - The locale key for the default Locale in our
     * environment is a zero length String.
     *
     * @param locale The locale for which a key is desired
     */
    protected function localeKey(Locale $locale) {
        return is_null($locale) ? "" : $locale->__toString();
    }

    /**
     * Load the messages associated with the specified Locale key.  For this
     * implementation, the <code>config</code> property should contain a fully
     * qualified package and resource name, separated by periods, of a series
     * of property resources to be loaded from the class loader that created
     * this PropertyMessageResources instance.  This is exactly the same name
     * format you would use when utilizing the <code>java.util.PropertyResourceBundle</code>
     * class.
     *
     * @param localeKey Locale key for the messages to be retrieved
     */
    protected function loadLocale($localeKey) {

        // Have we already attempted to load messages for this locale?
        if (array_key_exists($localeKey, $this->locales)) {
            return;
        }

        $this->locales[$localeKey] = $localeKey;

        // Set up to load the property resource for this locale key, if we can
        $name = str_replace('.', '/', $this->config);

        if (strlen($localeKey) > 0) {
            $name .= ("_" . $localeKey);
        }

        $name .= ".properties";

        // Load the specified property resource
        if ($this->log->isDebugEnabled()) {
            $this->log->debug("  Loading resource '" . $name . "'");
        }

        if(!Context::isReadable($name)) {
            $this->log->warn("  Resource ".$name." Not Found.");
            return;
        }

        $path = Context::getFilePath($name);

        $lines = file($path);
        if($lines !== false) {
            foreach ($lines as $i => $value) {
                $line = trim($lines[$i]);
                if(strlen($line) > 0) {
                    $key   = substr($line, 0, strpos($line, "="));
                    $value = substr($line, strpos($line, "=") + 1);
                    $this->messages[$this->messageKey($localeKey, $key)] = $value;
                }
            }
        }

    }

}
?>